#!/usr/bin/env python
#
# Copyright 2010 Sebible Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
"""A Model class hack to make it work with django_ldap queryset"""


from copy import deepcopy

from django.db import models
from django.db.models import signals
from django.utils.encoding import smart_unicode
from django.conf import settings
import logging
logger = logging.getLogger('django_ldap.models')


from django_ldap.query import delete_objects
from django_ldap import options
from django_ldap import managers


attr_black_list = ['id', ]


class Model(models.Model):
    objects = managers.Manager()
    _attr_black_list = ['id']

    class Meta:
        abstract = True

    def __init__(self, **kwargs):
        """
        Hack the __init__ function to store kwargs in _data
        """
        self._data = deepcopy(self._ldap_options.attr)

        # init _data with kwargs
        for key, value in kwargs.iteritems():
            if value:
                if key in self._ldap_options.attr_mapping.keys():
                    key = self._ldap_options.attr_mapping[key]
                if isinstance(value, list):
                    self._data[key] = value
                else:
                    self._data[key] = [value]

        # if objectClass is None, then set it to the default value
        if not self._data['objectClass']:
            self._data['objectClass'] = self._ldap_options.objectClass

        super(Model, self).__init__()

        # if dn exists, set the pk to dn
        if self.dn:
            self._set_pk_val(self.dn)

    def get_default_id(self):
        """docstring for get_id"""
        if self._ldap_options.dn_pattern:
            tmp_data = {}
            for key, value in self._data.items():
                if len(value) > 0:
                    tmp_data[key] = value[0]
            return '%s,%s' % ((self._ldap_options.dn_pattern % tmp_data), self._ldap_options.base)
        if self._ldap_options.rdn:
            for rdn in self._ldap_options.rdn:
                try:
                    rdn_value = getattr(self, rdn)
                    if isinstance(rdn_value, list):
                        rdn_value = rdn_value[0]
                    if rdn_value and rdn_value:
                        return '%s=%s,%s' % (rdn, rdn_value, self._ldap_options.base)
                except:
                    pass
        return None

    def __getattr__(self, key):
        """docstring for __getattr__"""
        if key in self._data.keys():
            if key in self._ldap_options.single_value_attr:
                if len(self._data[key]) > 0:
                    return self._data[key][0]
                else:
                    return u''
            else:
                return self._data[key]
        else:
            raise AttributeError(key)

    def __delattr__(self, key):
        del self._data[key]

    def __setattr__(self, key, value):
        """docstring for __setattr__"""
        if key.startswith('_'):
            return object.__setattr__(self, key, value)
        if hasattr(self, '_data') and (not key in self.__dict__.keys()) and \
            (not key in self._ldap_options.attr_mapping.keys()) and \
            (key not in self._attr_black_list) and \
            (not key.endswith('_ptr_id')):
            if isinstance(value, list):
                self._data[key] = map(smart_unicode, value)
            else:
                self._data[key] = [smart_unicode(value, strings_only=True)]
        else:
            return object.__setattr__(self, key, value)
 
    def _get_pk_val(self):
        return getattr(self, self._ldap_options.pk_attr)

    def _set_pk_val(self, value):
        return setattr(self, self._ldap_options.pk_attr, value)

    pk = property(_get_pk_val, _set_pk_val) 
    
    def save(self, raw=False, cls=None, force_insert=False,
                force_update=False):
        if not cls:
            cls = self.__class__
            meta = self._meta
            signal = True
            signals.pre_save.send(sender=self.__class__, instance=self, raw=raw)
        else:
            meta = cls._meta
            signal = False

        manager = self._default_manager

        # First, try an UPDATE. If that doesn't update anything, do an INSERT.
        pk_val = self._get_pk_val()

        # Note: the comparison with '' is required for compatibility with
        # oldforms-style model creation.
        pk_set = pk_val is not None and smart_unicode(pk_val) != u''
        record_exists = True
        if pk_set:
            try:
                record = manager.get(pk=pk_val)
                manager.filter(pk=pk_val).update(**self._data)
            except self.DoesNotExist:
                record_exists = False
        if not pk_set or not record_exists:
            self._data['dn'] = [self.get_default_id()]
            result = manager._insert(self._data)
            self._set_pk_val(result)

        if signal:
            signals.post_save.send(sender=self.__class__, instance=self,
                created=(not record_exists), raw=raw)

    def delete(self):
        assert self._get_pk_val() is not None, "%s object can't be deleted because its %s attribute is set to None." % (self._meta.object_name, self._meta.pk.attname)

        # Actually delete the objects
        delete_objects([self])

    def _add_relation(self, value, from_attr=None, to_attr=None, mode='from'):
        assert value, 'Variable value shouldn\'t be blank'
        if mode == 'to' or mode == 'both':
            assert to_attr, 'Variable to_attr shouldn\'t be None'
            items = getattr(self, to_attr, [])
            logger.debug('before add relation attr %s is %s' % (to_attr, items))
            if value not in items:
                items.append(value)
                logger.debug('add %s to attr %s' % (value, to_attr))
                setattr(self, to_attr, items)
                self.save()
        if mode == 'from' or mode == 'both':
            assert from_attr, 'Variable from_attr shouldn\'t be None'
            from django_ldap.models import GeneralModel
            far_end = GeneralModel.objects.get(pk=value)
            far_end._add_relation(self.pk, to_attr=from_attr, mode='to')          
    
    def _del_relation(self, value, from_attr=None, to_attr=None, mode='from'):
        assert value, 'Variable value shouldn\'t be blank'
        if mode == 'to' or mode == 'both':
            assert to_attr, 'Variable to_attr shouldn\'t be None'
            items = getattr(self, to_attr, [])
            try:
                items.remove(value)
            except ValueError:
                pass
            setattr(self, to_attr, items)
            self.save()
        if mode == 'from' or mode == 'both':  
            assert from_attr, 'Variable from_attr shouldn\'t be None'
            from django_ldap.models import GeneralModel
            far_end = GeneralModel.objects.get(pk=value)  
            far_end._del_relation(self.pk, to_attr=from_attr, mode='to')

    def as_dict(self):
        d={}
        for k in self._data.keys():
            try:
                d[k] = getattr(self, k)
                if isinstance(d[k], list):
                    d[k] = d[k][0]
            except:
                pass
        return d

class GeneralModel(Model):
    pass

def hash_password(raw_password, algorithm='md5'):
    """ Create a password string suitable for userPassword """
    import os, base64
    if algorithm == 'clear':
        return raw_password.strip()
    if algorithm == 'crypt':
        try:
            import crypt
        except ImportError:
            raise ValueError('"crypt" password algorithm not supported in this environment')
        salt = os.urandom(2)
        return ('{CYRPT}' + crypt.crypt(raw_password, salt)).strip()
    # The rest of the supported algorithms are supported by hashlib, but
    # hashlib is only available in Python 2.5.
    try:
        import hashlib
    except ImportError:
        if algorithm == 'md5':
            import md5
            return ('{MD5}' + base64.encodestring(md5.new(raw_password).digest())).strip()
    else:
        if algorithm == 'md5':
            return ('{MD5}' + base64.encodestring(hashlib.md5(raw_password).digest())).strip()
        elif algorithm == 'ssha':
            salt = os.urandom(4)
            h = hashlib.sha1(raw_password)
            h.update(salt)
            return ("{SSHA}" + base64.encodestring(h.digest() + salt)).strip()
    raise ValueError("Got unknown password algorithm type in password.")  

class Person(GeneralModel):
    class Ldap:
        filter = '(objectClass=person)'
        objectClass = 'person'

    def set_password(self, raw_password, algorithm='md5'):
        self.userPassword = hash_password(raw_password, algorithm)
    
    def check_password(self, raw_password):
        from django.conf import settings
        from backend import Backend
        backend_obj = Backend(server=settings.LDAP_SERVER,bind_dn=self.pk,bind_pwd=raw_password)
        try:
            c = backend_obj.get_connection()
        except:
            return False
        return True